查看原文
其他

使用 Material Design 组件实现深色主题

Google Play 谷歌开发者 2020-11-06

作者 / Chris Banes, Android 开发者关系团队工程师


Material 主题 (Theming) 涵盖的内容很多,但都旨在帮助开发者打造优良且富有表现力的用户体验,如果您对 Material 主题的颜色、字体和形状内容感兴趣的话,欢迎阅读 Nick Rout 的系列博文:

  • Material Design 组件: Android 上的颜色主题
    https://medium.com/androiddevelopers/material-theming-with-mdc-color-860dbba8ce2f
  • Material Design 组件: Android 上的字体主题
    https://medium.com/androiddevelopers/material-theming-with-mdc-type-8c2013430247
  • Material Design 组件: Android 上的形状主题
    https://medium.com/androiddevelopers/material-theming-with-mdc-shape-126c4e5cd7b4

本文将在此基础上,探讨如何调整应用以支持深色主题。


用户可选的深色主题在 Android 10 中被添加到 Android 平台,但应用开发者们应该早就接触过深色主题了: 在 Android 5.0 (Lollipop) 之前,Android 设备的默认主题都是深色!


  • Android 10
    https://developer.android.google.cn/about/versions/10
  • Android 5.0
    https://developer.android.google.cn/about/versions/lollipop

去年加入的深色主题的特别之处在于,平台增加了一个设备级别的设置,让用户可以对设备整体的主题进行控制,同时还能设置应用单独的主题。


除了最近加入的设备级别设置,在 material.io 上还提供了全面的设计指南,我们将在本文中详细介绍。


  • 设计指南: 深色主题
    https://material.io/design/color/dark-theme.html


为什么要支持深色主题?


首先,到底为什么要支持深色主题?在 material.io 上很好地总结了深色主题的技术优势 (我加粗了想要强调的部分):


深色主题可以降低设备屏幕发出的亮度 […]。能够通过减少用眼疲劳、根据当前照明条件调节亮度,以及让屏幕在黑暗环境中也便于观看等方式,改善视觉工效,同时还能降低电量消耗 [OLED 显示屏]。


  • Material Design: 深色主题
    https://material.io/design/color/dark-theme.html#usage

不过,最根本的原因是用户想要深色主题——这是用户们一直以来的首要需求,因此 Android 团队添加了系统级别的深色主题设置。


相信看到这里,您已经打算在应用中支持深色主题了,接下来就让我们来看看怎么实现它。



快速入门


要向应用添加深色主题,可以使用 Android 版的 Material Design 组件 (MDC)。


  • Material Design 组件: Android
    https://material.io/develop/android/

1. 更改主题
您需要更改主题,使其扩展自一个 Theme.MaterialComponents.DayNight 主题:
<style name="Theme.MyApp" parent="Theme.MaterialComponents.DayNight"> <!-- Other theme attributes --></style>

2. 选择模式 (可选)
这是可选步骤,可以支持 Android 10 之前版本的设备。由于 Android 10 之前大多数设备没有系统级的深色主题设置*,应用可以提供自己的应用内设置,允许用户按应用选择主题。

* 严格来说并不绝对是这样,因为有些设备制造商已经在运行 Android 9 (和更低版本) 的设备上添加了系统级深色主题。只不过这一点无法在运行时确定。

△ 应用内深色主题设置示例

这在 Android 10 及以上版本的系统中也很有用,因为这让用户可以根据需要覆盖系统设置。比如用户将设备主题设置为按时间调整,但又希望社交应用始终为深色主题。


为了做到这一点,(MDC 使用的) AppCompat 提供了一个 API 来设置模式: AppCompatDelegate.setDefaultNightMode()。通常,当偏好设置发生变化时就会调用这个 API。


  • AppCompat
    https://developer.android.google.cn/jetpack/androidx/releases/appcompat
  • setDefaultNightMode()
    https://developer.android.google.cn/reference/androidx/appcompat/app/AppCompatDelegate#setDefaultNightMode(int)

如果您想进一步了解 AppCompat 中夜间模式功能的运作细节,可以阅读这篇博文


  • DayNight - 在应用中添加深色主题
    https://medium.com/androiddevelopers/appcompat-v23-2-daynight-d10f90c83e94

3. 测试!
现在,深色主题的基础已经完成!接下来应该在浅色和深色主题下检查应用的各个部分。要注意深色背景上是否有深色文本,以及相对于深色背景对比度不足的硬编码颜色 (通常为灰色)。

如果您在应用中使用了硬编码颜色值,建议阅读 Nick Butcher 的《Android 样式系统 | 主题背景属性》或观看我们在 Android Dev Summit ‘19 的演讲《如何正确开发外观样式》。


Material 深色主题


现在来看看 material.io 上介绍的深色主题设计特征。


灰色与黑色
首先您可能会注意到,深色主题应用的默认背景不是黑色,而是深灰色: #121212。

关于我们为什么选择灰色而不是黑色的讨论有很多,尤其是考虑到 Android 10 平台使用的是黑色背景。这主要是我们在易用性与节能之间权衡的结果。

在平台中使用纯黑 #000000 色作为背景,可以让系统应用和表面 (surfaces) 在 OLED 显示屏上显示时消耗最少的电量。这些系统表面往往很简单,通常只有文本和简单的图标,因此可以根据需要调整文本和图标的颜色解决对比度问题。

不过在应用中,表面可以包含任何内容: 复杂的彩色矢量动画、明亮的图像、对比强烈的品牌表面等。在纯黑背景下会产生非常高的对比度,这会增加用眼疲劳。不同于先前提到的文本和图标,这些更复杂的内容通常很难或不适合为了降低对比度而调色/重新着色,因此稍浅一些的背景色更加合适。

调色板
接下来让我们来看看应用的调色板。您很可能是按照浅色/白色背景为应用选择的调色板,因此当应用以深色主题运行时,我们需要对调色板做出一些调整。

Material 颜色系统
我们先快速回顾一下 Material 颜色系统,稍后会详细谈及色调的问题。Material 颜色系统将颜色定义为每种颜色内的一系列色调。色调编号从 50 (最浅、饱和度最低的色调) 到 900 (最深、饱和度最高的色调)。这是基准蓝绿色和靛蓝色调:
△ 基准 Material 调色板

您也可以在 Material 颜色工具中上手操作,了解不同颜色的色调如何变化。Nick Rout 也在这篇文章里对颜色系统进行了深度剖析。


  • Material 颜色工具
    https://material.io/resources/color/
  • Material Design 组件: Android 上的颜色主题
    https://medium.com/androiddevelopers/material-theming-with-mdc-color-860dbba8ce2f

colorPrimary

应用的主色是显示最多的颜色 (除了背景和表面颜色),所以我们需要确保它在深色主题中清晰可辨。通常,浅色主题会是一个 500 色调的颜色,而在深色主题中,我们建议使用饱和度较低、亮度较高的色调,一般为 200,但根据不同色相最多可以达到 50


对于 colorPrimaryVariant,我们建议使用浅色主题中的 colorPrimary。下面是一个简单的参考表格:

这些值只是调整的基础。您应该确保所选颜色在所有使用的高程上与背景/表面颜色的 WCAG AA 对比度至少为 4.5:1 (后文会详细介绍)。

  • WCAG AA
    https://www.w3.org/WAI/standards-guidelines/wcag/

Material 颜色工具非常适合尝试新颜色。

  • Material 颜色工具
    https://material.io/resources/color/

colorSecondary
对于辅色,和 colorPrimary 的处理方法一样,使用饱和度较低、亮度较高的同色色调。

基准 Material 深色主题对待 colorSecondaryVariant 与 colorPrimaryVariant 的方式有些不同,对 colorSecondary 和 colorSecondaryVariant 使用相同的色调。

下面是另一个简单的参考表格:

表面颜色
大胆的彩色表面是在卡片等常用组件中表达品牌的好方法。虽然鲜艳大胆的颜色在白色背景下效果很好,但在深色背景下可能就是另一回事了。

  • 在 UI 中使用颜色
    https://material.io/design/color/applying-color-to-ui.html#sheets-and-surfaces

如果设备和/或应用已经设置为使用深色主题,意味着用户在那一刻想要一个不太花哨的柔和配色方案。


考虑到这种意图,即使我们在品牌表面使用 50-200 的柔和色调,对于深色主题来说,它仍然可能过于鲜艳和明亮:

不要这样做。演示中底部应用栏的表面颜色过于明艳

那么您应该怎么做呢?以下两个选项可以结合使用:

1. 使用主表面
第一步当然是在深色主题下不使用明亮、多彩的表面。Android 版 Material Design 组件使用 PrimarySurface 样式简化了这一过程,您可以在浅色主题中使用 Primary 颜色,在深色主题中使用 Surface 颜色。

下面我们来看一个示例。假设我们有一个像上个示例一样的 BottomAppBar,那么可以使用 Widget.MaterialComponents.BottomAppBar.PrimarySurface 样式:
<com.google.android.material.bottomappbar.BottomAppBar style="Widget.MaterialComponents.BottomAppBar.PrimarySurface"/>

  • BottomAppBar
    https://material.io/develop/android/components/bottom-app-bars/

如果您想对一个非 MDC 视图进行类似处理,可以使用 ?attr/colorPrimarySurface 主题属性:

<FrameLayout android:background="?attr/colorPrimarySurface"/>

△ 通过样式和属性实现 PrimarySurface 的示例
事实上,在浅色主题中使用明亮表面颜色的组件 (如 MaterialToolbar) 默认具有同样的行为。因此您在使用它们时可能不需要做任何工作。

  • MaterialToolbar
    https://material.io/develop/android/components/top-app-bars/

2. 使用品牌表面颜色
要在应用的所有表面含蓄地表现品牌色彩,您可以在深色主题时将 colorSurface 设置为不透明度 8% 的 colorPrimary 与 colorSurface 叠加之后的颜色。

例如,使用基准主题的色值:

△ 如何计算品牌表面颜色

这样一来,您就可以在遵循柔和、低亮度颜色意图的前提下,将品牌颜色巧妙地融入整个应用。


制作深色主题的示例
如果您想查看向浅色主题应用加入深色主题的示例,可以观看 Liam Spradlin 的视频,了解如何为 Reply 这个 Material 教学应用添加深色主题。

  • 使用 Material Design 打造深色主题
    https://youtu.be/hbJmm-d94FA
  • Reply 应用
    https://material.io/design/material-studies/reply.html

我们已经介绍过许多有关选择颜色的知识,但是如何在 Android 应用中进行设置呢?


我们要搭建一个主题的结构。如下所示:

△ 适合深色主题的主题结构

这种结构让我们可以轻松地在浅色和深色主题中改变主题,还允许我们在基础主题中重用常见的内容。


如果您想进一步了解这种结构,建议观看去年 Nick Butcher 和我的演讲《如何正确开发外观样式》。

<style name="Base.Theme.Tivi" parent="Theme.MaterialComponents.DayNight"> <!-- Your app theme, minus color palette --></style>
<style name="Theme.Tivi" parent="Base.Theme.Tivi"> <item name="colorPrimary">@color/slate_500</item> <item name="colorOnPrimary">#000000</item> <item name="colorSecondary">@color/orange_500</item> <item name="colorOnSecondary">#000000</item></style>
△ values/themes.xml
<style name="Base.Theme.Tivi" parent="Theme.MaterialComponents.DayNight"> <!-- Your app theme, minus color palette --></style>
<style name="Theme.Tivi" parent="Base.Theme.Tivi"> <item name="colorSurface">@color/slate_200_8pc_surface</item> <item name="colorPrimary">@color/slate_200</item> <item name="colorOnPrimary">#000000</item> <item name="colorSecondary">@color/orange_00</item> <item name="colorOnSecondary">#000000</item></style>

△ values-night/themes.xml



高度叠加层


前文已经提到了需要针对所有高程进行对比度测试。您可能会困惑,毕竟高程是关于提升表面来投射阴影的吧?没错,高程是与提升表面有关,但不仅仅是为了投射阴影。


Material 系统中的阴影是由许多光源投射形成的,当我们 (使用高程属性) 提升表面时,是在把它们朝着光源提升。和我们的现实世界一样,当这些光源被表面遮挡时就会出现阴影。同样,表面离光源越近,表面被照亮的程度就越高,从而改变了呈现的颜色。


  • 光源
    https://material.io/design/environment/light-shadows.html#light

对于白色等浅色表面,这种变化不易察觉。但在深色表面上则会产生很大的影响:

△ 不同高程的高度叠加层演示

这就是高度叠加层起作用的地方。提亮表面颜色的行为表现为在表面颜色上叠加一个半透明的白色 onSurface 层。高程越大,叠加层越不透明,表面也就越亮。


  • 高度叠加层
    https://material.io/design/color/dark-theme.html#properties

这就是先前提到的需要在不同高程进行测试的原因。由于视觉表面会根据高程变化,您需要确保所有前景色都能提供足够的对比度。理想情况下,可以设置一个单一 onSurface 颜色来适用于应用中的所有高程。



Widget 支持


MDC 中的所有组件都自动支持高度叠加层,包括: 顶部应用栏底部应用栏底部导航标签页卡片对话框菜单底部动作条抽屉式导航栏开关


  • 顶部应用栏
    https://material.io/components/android/catalog/top-app-bars/
  • 底部应用栏
    https://material.io/components/android/catalog/bottom-app-bars/
  • 底部导航
    https://material.io/components/android/catalog/bottom-navigation/
  • 标签页
    https://material.io/components/android/catalog/tab-layout/
  • 卡片
    https://material.io/components/android/catalog/cards/
  • 对话框
    https://material.io/components/android/catalog/dialogs/
  • 菜单
    https://material.io/components/android/catalog/menu/
  • 底部动作条
    https://material.io/components/android/catalog/bottom-sheet-behavior/
  • 抽屉式导航栏
    https://material.io/components/android/catalog/navigation-view/
  • 开关
    https://material.io/components/android/catalog/switches/

因此,只要背景设置为 ?attr/colorSurface (显式使用或使用表面样式变体),使用标准高程 API 就会自动应用高度叠加层。回到我们前面的示例:

<!-- 👍 Elevation overlay is applied: we're using colorSurface --><com.google.android.material.bottomappbar.BottomAppBar android:elevation="2dp"    android:background="?attr/colorSurface"/>
<!-- 👍 Elevation overlay is applied: Surface style uses colorSurface --><com.google.android.material.bottomappbar.BottomAppBar android:elevation="2dp" style="@style/Widget.MaterialComponents.BottomAppBar.Surface"/>
<!-- ❌ No elevation overlay applied: we're using colorSecondary --><com.google.android.material.bottomappbar.BottomAppBar android:elevation="2dp" android:background="?attr/colorSecondary"/>
<!-- ❌ No elevation overlay applied: we're using colorPrimary --><com.google.android.material.bottomappbar.BottomAppBar android:elevation="2dp" style="@style/Widget.MaterialComponents.BottomAppBar.Primary"/>

您可以设置一些主题属性来更改高度叠加层的行为:
  • ?attr/elevationOverlayEnabled 允许您打开/关闭主题的高度叠加层。深色主题默认为 true,浅色主题默认为 false。
  • ?attr/elevationOverlayColor 允许您改变任何高度叠加层的颜色。默认为 ?attr/colorOnSurface。

不过,您实际上不需要更改它们。


自定义视图


如果需要支持高度叠加层的自定义视图怎么办?告诉您一个好消息: MaterialShapeDrawable 直接支持高度叠加层,只需要在视图中进行一点改动即可:

class CustomSurfaceView : View { private val shapeDrawable = MaterialShapeDrawable()
init { background = shapeDrawable shapeDrawable.initializeElevationOverlay(context)    }        override fun onAttachedToWindow() {     super.onAttachedToWindow()     // Update the shape drawable with this view's absolute     // elevation value in the view hierarchy     MaterialShapeUtils.setParentAbsoluteElevation(this, shapeDrawable)    }
override fun setElevation(elevation: Float) { super.setElevation(elevation) onZChanged()    }        override fun setTranslationZ(translationZ: Float) {     super.setTranslationZ(translationZ)     onZChanged()    }        override fun setZ(z: Float) {     super.setTranslationZ(translationZ)     onZChanged()    }        private fun onZChanged() {     // Tell the ShapeDrawable what our new Z value is     shapeDrawable.z = z    }}

  • MaterialShapeDrawable
    https://developer.android.google.cn/reference/com/google/android/material/shape/MaterialShapeDrawable


OK Google,晚安 🌚


希望本文可以让您了解如何为应用添加深色主题。欢迎在下方评论区留言分享您在实现深色主题时遇到过的问题。



推荐阅读






 点击屏末 | 阅读原文 | 查看 Material Design 设计指南



    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存